/**
 * Reads only up to version 6 (one alignment pattern, no version info)
 */
package com.squeekeek.util.qrcode;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;

import org.opencv.core.Core;
import org.opencv.core.Core.MinMaxLocResult;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.highgui.Highgui;
import org.opencv.imgproc.Imgproc;

import android.util.Log;

public class QRAlgorithm {
	// Static constants
	private static final int BYTE_BLACK = 0x00;
	private static final int BYTE_WHITE = 0xFF;
	
	private static boolean isOriented = false;

	private static Mat imgMatBin = null;
	private static Mat imgMatBW = null;
	private static List <Point> imgFinderLoc = null;
	private static List <Byte> dataStream = null;
	private static List <Byte> dataWords = null;
	private static List <Byte> ecWords = null;
	private static String appStorageDir = null;
	
	private static double codeWidth = -1;
	private static double modWidth = -1;
	private static int versionInfo = -1;
	private static int fmtClass = -1;
	
	public QRAlgorithm(String imgName) throws QRCodeException {
		try {
			imgMatBin = Highgui.imread(imgName, Highgui.CV_LOAD_IMAGE_GRAYSCALE);
			appStorageDir = new File(imgName).getParentFile().getAbsolutePath();
			System.out.println(imgName);
		}
		catch(Exception e) {
			throw new QRCodeException("Cannot read image: " + e.getMessage());
			//Log.d("QRCode", "Cannot read image: " + e.getMessage());
		}
		
		imgMatBW = null;
		imgFinderLoc = null;
		isOriented = false;
	}
	
	public String saveThreshImg() throws QRCodeException {
		String imgPathStr;
		
		drawFinderLoc();
		connectTheDots();
		imgPathStr =  appStorageDir + File.separator + "postprocess.jpg";
		
		if(!Highgui.imwrite(imgPathStr, imgMatBW)) {
			throw new QRCodeException("Failed to save threshold image.");
			//Log.d("QRCode", "Failed to save threshold image.");
		}
		
		return imgPathStr;
	}
	
	public void localThreshBernsen() {
		int threshVal;
		int pixVal;
		Mat threshWin;
		MinMaxLocResult localMinMax;
		
		if(imgMatBW != null) {
			Log.d("QRCode", "Image threshold already exists.");
			return;
		}
		
		imgMatBW = new Mat(imgMatBin.size(), CvType.CV_8UC1);
		
		// Use a default window of 31
		for(int i = 0; i < imgMatBin.rows(); i++) {
			for(int j = 0; j < imgMatBin.cols(); j++) {
				int rowMin = i - 15;
				int rowMax = i + 15;
				int colMin = j - 15;
				int colMax = j + 15;
				
				if(rowMin < 0) {
					rowMin = 0;
				}
				
				if(rowMax >= imgMatBin.rows()) {
					rowMax = (int) imgMatBin.rows() - 1;
				}
				
				if(colMin < 0) {
					colMin = 0;
				}
				
				if(colMax >= imgMatBin.cols()) {
					colMax = (int) imgMatBin.cols() - 1;
				}

				threshWin = imgMatBin.submat(rowMin, rowMax, colMin, colMax);
				localMinMax = Core.minMaxLoc(threshWin);
				threshVal = (int) (localMinMax.maxVal + localMinMax.minVal) / 2;
				pixVal = (int) imgMatBin.get(i, j)[0];
				
				if(pixVal < threshVal) {
					imgMatBW.put(i, j, BYTE_BLACK);
				}
				else {
					imgMatBW.put(i, j, BYTE_WHITE);
				}
			}
		}
	}
	
	public void getFinderPatterns() throws QRCodeException {
		int nowState;
		int pixVal;
		int totalRowPixels;
		int totalColPixels;
		int rowCenter;
		int colCenter;
		int[] pixelCount = {0, 0, 0, 0, 0};
		Vector <Integer[]> rowRange;
		
		if(imgMatBW == null) {
			Log.d("QRCode", "Haven't thresholded an image.");
			throw new QRCodeException("Haven't thresholded an image.");
		}
		
		
		if(imgFinderLoc != null) {
			Log.d("QRCode", "Position detector patterns already read.");
			throw new QRCodeException("Position detector patterns already read.");
		}
		
		nowState = 0;
		imgFinderLoc = new ArrayList <Point> ();
		
		// QR Ratio: 1:1:3:1:1 = B:W:B:W:B
		// Scan each row, skipping 5 rows
		for(int i = 0; i < imgMatBW.rows(); i += 5) {
			for(int j = 0; j < imgMatBW.cols(); j++) {
				pixVal = (int) imgMatBW.get(i, j)[0];

				if(nowState == 4 && pixVal == BYTE_WHITE) {
					totalRowPixels = 0;
					
					for(int r : pixelCount) {
						totalRowPixels += r;
					}
					
					if(checkFinderRatio(pixelCount)) {
						rowRange = tryFinderCol(j - totalRowPixels + 1 + totalRowPixels / 2);
						colCenter = j - totalRowPixels + 1 + totalRowPixels / 2;
						
						for(Iterator <Integer[]> it = rowRange.iterator(); it.hasNext(); ) {
							Integer[] eachRowCoord = it.next();
							
							if(eachRowCoord[0] <= i && i <= eachRowCoord[1]) {
								totalColPixels = eachRowCoord[1] - eachRowCoord[0] + 2;
								rowCenter = eachRowCoord[1] - totalColPixels + 1 + totalColPixels / 2;
								
								if(checkLeftDiagRatio(rowCenter, colCenter) && isUniqueFinderLoc(rowCenter, colCenter)) {
									imgFinderLoc.add(new Point(colCenter, rowCenter));
								}
							}
						}
						
						nowState = 0;
						
						for(int r = 0; r < 5; r++) {
							pixelCount[r] = 0;
						}
					}
					else {
						// Encountered BYTE_WHITE. Let the third state point to the first one.
						nowState = 3;
						
						pixelCount[0] = pixelCount[2];
						pixelCount[1] = pixelCount[3];
						pixelCount[2] = pixelCount[4];
						pixelCount[3] = 1;
						pixelCount[4] = 0;
					}
				}
				else if((nowState == 0 || nowState == 2) && pixVal == BYTE_WHITE) {
					nowState++;
				}
				else if((nowState == 1 || nowState == 3) && pixVal == BYTE_BLACK) {
					nowState++;
				}
				
				pixelCount[nowState]++;
			}
			
			nowState = 0;
			
			for(int r = 0; r < 5; r++) {
				pixelCount[r] = 0;
			}
		}
	}
	
	public void setOrientation() throws QRCodeException {
		int maxIdx;
		int ulIdx;
		int urIdx;
		int slopeSign;
		double myDist;
		double maxSlope;
		double crossP;
		double angleRot;
		double maxLen;
		Point codeCenter;
		Point[] transFinderLoc;
		MatOfPoint pointMat;
		MatOfPoint2f pointMatF;
		MatOfPoint2f transFinderMat;
		Mat vecA, vecB;
		Mat rotMat, transMat;
		Mat myVec;
		
		if(imgFinderLoc == null) {
			Log.d("QRCode", "You haven't read some alignment patterns yet.");
			throw new QRCodeException("You haven't read some alignment patterns yet.");
		}
		
		if(imgFinderLoc.size() != 3) {
			Log.d("QRCode", "Cannot set orientation. There are " + imgFinderLoc.size() + " finder patterns read.");
			throw new QRCodeException("Cannot set orientation since we read " + imgFinderLoc.size() + " finder patterns.");
		}
		
		if(isOriented == true) {
			Log.d("QRCode", "QR Code has been oriented properly.");
			throw new QRCodeException("QR Code has already been oriented properly.");
		}
		
		pointMat = new MatOfPoint();
		pointMat.fromList(imgFinderLoc);
		
		myVec = new Mat(1, 2, CvType.CV_8UC1);
		maxLen = -1;
		maxIdx = -1;
		
		// Find hypotenuse between the three alignment patterns
		for(int i = 0; i < 3; i++) {
			Core.subtract(pointMat.row(i), pointMat.row((i + 1) % 3), myVec);
			myDist = Core.norm(myVec, Core.NORM_L2);
			
			if((int) (10000 * (myDist - maxLen)) > 0) {
				maxLen = myDist;
				maxIdx = i;
			}
		}
		
		codeWidth = maxLen / Math.sqrt(2);
		
		vecA = new Mat();
		vecB = new Mat();
		
		// Determine ordering of points
		ulIdx = (maxIdx + 2) % 3;
		Core.subtract(pointMat.row((ulIdx + 1) % 3), pointMat.row(ulIdx), vecA);
		Core.subtract(pointMat.row((ulIdx + 2) % 3), pointMat.row(ulIdx), vecB);
		crossP = vecA.get(0, 0)[0] * vecB.get(0, 0)[1] - vecB.get(0, 0)[0] * vecA.get(0, 0)[1];
		
		if(Math.signum(crossP) == -1.0) {
			// Negative cross product, vecA is left part with (ulIdx + 1) % 3 the LL part 
			urIdx = (ulIdx + 2) % 3;
		}
		else {
			urIdx = (ulIdx + 1) % 3;
		}
		
		// Reorder imgPosDecLoc such that the zeroth index is the upper-left pattern
		// successive patters are in ccw order
		Collections.swap(imgFinderLoc, ulIdx, 0);
		
		if(urIdx == 0) {
			urIdx = ulIdx;
		}
		
		Collections.swap(imgFinderLoc, urIdx, 2);
		
		// Get angle of inclination
		maxSlope = (imgFinderLoc.get(1).y - imgFinderLoc.get(2).y) / (imgFinderLoc.get(1).x - imgFinderLoc.get(2).x);
		
		if((int) (1000 * Math.abs(maxSlope)) < 1000) {
			slopeSign = 1;
		}
		else {
			slopeSign = -1;
		}

		angleRot = Math.atan(slopeSign * (maxSlope - 1) / (1 + maxSlope)) * 180 / Math.PI;
		codeCenter = new Point(Math.abs(imgFinderLoc.get(1).x - imgFinderLoc.get(2).x) / 2 + Math.min(imgFinderLoc.get(1).x, imgFinderLoc.get(2).x), Math.abs(imgFinderLoc.get(1).y - imgFinderLoc.get(2).y) / 2 + Math.min(imgFinderLoc.get(1).y, imgFinderLoc.get(2).y));
		
		rotMat = Imgproc.getRotationMatrix2D(codeCenter, 90 * slopeSign + angleRot, 1);
		Imgproc.warpAffine(imgMatBW, imgMatBW, rotMat, imgMatBW.size(), Imgproc.INTER_NEAREST, Imgproc.BORDER_TRANSPARENT, new Scalar(0x0));
		
		// Transform points according to rotation
		for(ListIterator<Point> it = imgFinderLoc.listIterator(); it.hasNext(); ) {
			Point iElm;
			Point iTrans;
			Mat ptTrans;
			Mat ptMat;
			
			iElm = it.next();
			
			ptMat = new Mat(3, 1, CvType.CV_64FC1);
			ptMat.put(0, 0, iElm.x);
			ptMat.put(1, 0, iElm.y);
			ptMat.put(2, 0, 1.0);
			
			ptTrans = new Mat(3, 1, CvType.CV_64FC1);
			iTrans = new Point();
			
			// Rotate
			Core.gemm(rotMat, ptMat, 1.0, new Mat(), 0.0, ptTrans);
			
			iTrans.x = ptTrans.get(0, 0)[0];
			iTrans.y = ptTrans.get(1, 0)[0];
			
			it.set(iTrans);
		}
		
		// Straighten the image
		pointMatF = new MatOfPoint2f();
		pointMatF.fromList(imgFinderLoc);
		
		transFinderLoc = new Point[3];
		transFinderMat = new MatOfPoint2f();
		
		transFinderLoc[0] = new Point(imgFinderLoc.get(0).x, imgFinderLoc.get(0).y);
		transFinderLoc[1] = new Point(imgFinderLoc.get(0).x, imgFinderLoc.get(0).y + codeWidth);
		transFinderLoc[2] = new Point(imgFinderLoc.get(0).x + codeWidth, imgFinderLoc.get(0).y);
		transFinderMat.fromArray(transFinderLoc);
		
		transMat = Imgproc.getAffineTransform(pointMatF, transFinderMat);
		Imgproc.warpAffine(imgMatBW, imgMatBW, transMat, imgMatBW.size(), Imgproc.INTER_NEAREST, Imgproc.BORDER_TRANSPARENT, new Scalar(0x0));
		
		// Transform points according to warp
		for(ListIterator<Point> it = imgFinderLoc.listIterator(); it.hasNext(); ) {
			Point iElm;
			Point iTrans;
			Mat ptTrans;
			Mat ptMat;
			
			iElm = it.next();
			
			ptMat = new Mat(3, 1, CvType.CV_64FC1);
			ptMat.put(0, 0, iElm.x);
			ptMat.put(1, 0, iElm.y);
			ptMat.put(2, 0, 1.0);
			
			ptTrans = new Mat(3, 1, CvType.CV_64FC1);
			iTrans = new Point();
			
			// Warp
			Core.gemm(transMat, ptMat, 1.0, new Mat(), 0.0, ptTrans);
			
			iTrans.x = ptTrans.get(0, 0)[0];
			iTrans.y = ptTrans.get(1, 0)[0];
			
			it.set(iTrans);
		}
		
		// Set orientation flag
		isOriented = true;
	}
	
	public void getVerFmtInfo() throws QRCodeException {
		int version;
		int pixVal;
		int offset;
		int fmtInfo;
		int fmtInfoExtra;
		int fmtA, fmtB;
		double squareWidth;
		
		if(isOriented == false) {
			Log.d("QRCode", "Set the orientation first.");
			throw new QRCodeException("Set the orientation first before decoding the format info.");
		}
		
		if(fmtClass != -1 && versionInfo != -1) {
			Log.d("QRCode", "Format and version information already read.");
			throw new QRCodeException("Format and version information already read.");
		}
		
		// Get module width and barcode version
		squareWidth = getModuleWidth();
		
		if(squareWidth == -1) {
			Log.d("QRCode", "Cannot get module width.");
			throw new QRCodeException("Cannot determine module width.");
		}
		
		version = (int) Math.round(((codeWidth / squareWidth) - 10) / 4);
		
		if(version >= 7) {
			Log.d("QRCode", "Sorry. The program supports only decoding of QR Codes with at most one alignment pattern. :(");
			return;
		}
		
		offset = 0;
		fmtInfo = 0;
		
		// Read format info at upper-left part of the barcode
		for(int i = 0; i < 17; i++) {
			if(i == 6 || i == 10) {
				// Skip these pixels
				i++;
				offset++;
			}
			
			if(i <= 8) {
				pixVal = (int) imgMatBW.get((int) (imgFinderLoc.get(0).y - (3 - i) * squareWidth), (int) (imgFinderLoc.get(0).x + 5 * squareWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfo |= 0x00 << (i - offset);
				}
				else {
					fmtInfo |= 0x01 << (i - offset);
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(0).x + 5 * squareWidth, imgFinderLoc.get(0).y - (3 - i) * squareWidth), 1, new Scalar(0x70), -1);
			}
			else {
				pixVal = (int) imgMatBW.get((int) (imgFinderLoc.get(0).y + 5 * squareWidth), (int) (imgFinderLoc.get(0).x + (4 + (9 - i)) * squareWidth))[0];

				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfo |= 0x00 << (i - offset);
				}
				else {
					fmtInfo |= 0x01 << (i - offset);
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(0).x + (4 + (9 - i)) * squareWidth, imgFinderLoc.get(0).y + 5 * squareWidth), 1, new Scalar(0x70), -1);
			}
		}
		
		offset = 0;
		fmtInfoExtra = 0;
		
		// Read format info at upper-right and lower-left parts of the barcode
		for(int i = 0; i < 16; i++) {
			if(i == 8) {
				i++;
				offset++;
			}
			
			if(i < 8) {
				pixVal = (int) imgMatBW.get((int) (imgFinderLoc.get(2).y + 5 * squareWidth), (int) (imgFinderLoc.get(2).x + (3 - i) * squareWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfoExtra |= 0x00 << i;
				}
				else {
					fmtInfoExtra |= 0x01 << i;
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(2).x + (3 - i) * squareWidth, imgFinderLoc.get(2).y + 5 * squareWidth), 1, new Scalar(0x70), -1);
			}
			else {
				pixVal = (int) imgMatBW.get((int) (imgFinderLoc.get(1).y - (3 + (9 - i)) * squareWidth), (int) (imgFinderLoc.get(1).x + 5 * squareWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfoExtra |= 0x00 << (i - offset);
				}
				else {
					fmtInfoExtra |= 0x01 << (i - offset);
				}

				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(1).x + 5 * squareWidth, imgFinderLoc.get(1).y - (3 + (9 - i)) * squareWidth), 1, new Scalar(0x70), -1);
			}
		}
		
		// Decode format info
		fmtA = QRErrorCorrection.decodeFmt(fmtInfo);
		fmtB = QRErrorCorrection.decodeFmt(fmtInfoExtra);
		
		// Pick which one is correctly decoded
		fmtClass = fmtA != -1 ? fmtA : fmtB;
		versionInfo = version;
		modWidth = squareWidth;
	}
	
	public void readData() throws QRCodeException {
		int pixVal;
		int bytePos;
		boolean readUp;
		byte dataByte;
		
		if(fmtClass == -1 || versionInfo == -1) {
			throw new QRCodeException("Read format and version info first before reading data.");
			//Log.d("QRCode", "Haven't read format and version info.");
		}
		
		if(dataStream != null) {
			throw new QRCodeException("Data stream already read.");
			//Log.d("QRCode", "Data stream already read.");
		}
		
		dataStream = new ArrayList <Byte> ();
		readUp = true;
		dataByte = 0;
		bytePos = 0;
		
		// Start at lower-right portion and read alternately upwards and downwards
		// (i, j) -> (col, row)
		int j = (int) Math.round(imgFinderLoc.get(1).y + 3 * modWidth);
		
		for(int i = (int) Math.round(imgFinderLoc.get(2).x + 3 * modWidth); i >= (int) Math.round(imgFinderLoc.get(0).x - 3 * modWidth); i -= Math.round(2 * modWidth)) {

			// Reverse reading direction
			if(isQuietZone(i, j)) {
				readUp = !readUp;
			}
			
			// Skip rows with functional parts until you get to the data encoding part
			while(isQuietZone(i, j) || isFunctional(i, j)) {
				if(readUp) {
					j -= (int) Math.round(modWidth);
				}
				else {
					j += (int) Math.round(modWidth);
				}
				
				if(j < 0 || j > imgMatBW.rows()) {
					Log.d("QRCode", "Anyare? Locked at quiet zone...");
					return;
				}
			}
			
			// Start data reading
			while(!isQuietZone(i, j)) {
				
				// Check horizontal timing pattern
				if(j == (int) Math.round(imgFinderLoc.get(0).y + 3 * modWidth)) {
					// Skip this line
					if(readUp) {
						j -= (int) Math.round(modWidth);
					}
					else {
						j += (int) Math.round(modWidth);
					}
				}
				
				// Read right. Check for vertical timing pattern
				if(!isFunctional(i, j) && !isQuietZone(i, j) && (i != (int) Math.round(imgFinderLoc.get(0).x + 3 * modWidth))) {
					pixVal = (int) imgMatBW.get(j, i)[0];
					
					// Check white pixel
					if(pixVal > 128) {
						dataByte |= (maskDataBit(i, j) ? 0x01 : 0x00) << (7 - bytePos);
					}
					else {
						dataByte |= (maskDataBit(i, j) ? 0x00 : 0x01) << (7 - bytePos);
					}
					
					if(bytePos % 8 == 7) {
						dataStream.add(dataByte);
						
						dataByte = 0;
						bytePos = 0;
					}
					else {
						bytePos++;
					}
					
					// Core.circle(imgMatBW, new Point(i, j), 1, new Scalar(0x45), -1);
				}

				// Read left. Check for vertical timing pattern
				i -= (int) Math.round(modWidth);
				
				if(!isFunctional(i, j) && !isQuietZone(i, j) && (i != (int) Math.round(imgFinderLoc.get(0).x + 3 * modWidth))) {
					pixVal = (int) imgMatBW.get(j, i)[0];
					
					// Check white pixel
					if(pixVal > 128) {
						dataByte |= (maskDataBit(i, j) ? 0x01 : 0x00) << (7 - bytePos);
					}
					else {
						dataByte |= (maskDataBit(i, j) ? 0x00 : 0x01) << (7 - bytePos);
					}
					
					if(bytePos == 7) {
						dataStream.add(dataByte);
						
						dataByte = 0;
						bytePos = 0;
					}
					else {
						bytePos++;
					}
					
					// Core.circle(imgMatBW, new Point(i, j), 1, new Scalar(0x45), -1);
				}
				
				// Revert pointer to right
				i += (int) Math.round(modWidth);
				
				if(readUp) {
					j -= (int) Math.round(modWidth);
				}
				else {
					j += (int) Math.round(modWidth);
				}
			}
			
			// By then, pointer should have gone at the quiet zone
		}
	}
	
	public void decodeData() throws QRCodeException {
		int lastDiff;
		byte prevBlock;
		byte ecLevel;
		byte[] dataBlocks;
		
		if(dataStream == null) {
			throw new QRCodeException("Read data stream before decoding it.");
			//Log.d("QRCode", "Data stream not yet read.");
		}
		
		prevBlock = -1;
		lastDiff = 0;
		dataWords = new ArrayList <Byte> ();
		ecWords = new ArrayList <Byte> ();
		
		
		ecLevel = (byte) ((fmtClass & 0x18) >>> 3);
		dataBlocks = QRErrorCorrection.getBlockData(ecLevel, versionInfo);
		
		System.out.println("Code Version: " + versionInfo);
		System.out.println("Format Version: " + Integer.toBinaryString(fmtClass & 0xFF));
		System.out.println("Total Codewords: " + dataStream.size());
		System.out.println("Module Size: " + modWidth);
		System.out.println("Image Size: " + imgMatBW.size().toString());

		// Deinterlace data codewords
		for(int i = 0; i < dataBlocks.length; i++) {
			if(prevBlock != -1 && prevBlock != dataBlocks[i]) {
				lastDiff = i;
			}
			
			for(int j = 0; j < dataBlocks[i]; j++) {
				if(j == dataBlocks[i] - 1) {
					// Offset for 5Q and 5H QR Codes
					dataWords.add(dataStream.get(i + j * dataBlocks.length - lastDiff));
				}
				else {
					dataWords.add(dataStream.get(i + j * dataBlocks.length));
				}
			}

			prevBlock = dataBlocks[i];
		}
		
		// Deinterlace error correction codewords
		for(int i = 0; i < dataBlocks.length; i++) {
			for(int j = 0; j < ((dataStream.size() - dataWords.size()) / dataBlocks.length); j++) {
				ecWords.add(dataStream.get(i + j * dataBlocks.length + dataWords.size()));
			}
		}
		
		System.out.println("Data codewords: " + dataWords.size());
		for(int i = 0; i < dataWords.size(); i++) {
			System.out.print(Integer.toHexString(dataWords.get(i) & 0xFF) + " ");
		}
		System.out.println("");
		
		System.out.println("Error correction codewords: " + ecWords.size());
		for(int i = 0; i < ecWords.size(); i++) {
			System.out.print(Integer.toHexString(ecWords.get(i) & 0xFF) + " ");
		}
		System.out.println("");
	}
	
	private static boolean maskDataBit(int x, int y) {
		int codeI;
		int codeJ;
		byte maskPatt;
		
		if(fmtClass == -1 || versionInfo == -1) {
			Log.d("QRCode", "Haven't read format and version info yet");
		}
		
		// The last three bits of fmtClass denote the mask pattern
		maskPatt = (byte) (fmtClass & 0x07);
		codeI = (y - (int) (imgFinderLoc.get(0).y - 3 * modWidth)) / (int) modWidth;
		codeJ = (x - (int) (imgFinderLoc.get(0).x - 3 * modWidth)) / (int) modWidth;
		
		switch(maskPatt) {
		case 0x00:
			return (codeI + codeJ) % 2 == 0;
		case 0x01:
			return codeI % 2 == 0;
		case 0x02:
			return codeJ % 3 == 0;
		case 0x03:
			return (codeI + codeJ) % 3 == 0;
		case 0x04:
			return ((codeI / 2) + (codeJ / 3)) % 2 == 0;
		case 0x05:
			return (codeI * codeJ) % 2 + (codeI * codeJ) % 3 == 0;
		case 0x06:
			return ((codeI * codeJ) % 2 + (codeI * codeJ) % 3) % 2 == 0;
		case 0x07:
			return ((codeI * codeJ) % 3 + (codeI + codeJ) % 2) % 2 == 0;
		}

		return false;
	}
	
	public String dumpData() {
		String data = null;
		
		data = "";
		data += "Finder patterns: " + imgFinderLoc.size() + "\n";
		/**
		data += "QR Code Version: " + versionInfo + QRErrorCorrection.ecFmtToString((byte) ((fmtClass & 0x18) >>> 3)) + "\n";
		data += "Data Codewords: " + ecWords.size() + "\n";
		data += "Data:\n";
		for(int i = 0; i < ecWords.size(); i++) {
			data += Integer.toHexString(ecWords.get(i) & 0xFF) + " ";
		}
		data += "\n";
		**/
		
		return data;
	}
	
	private static boolean isFunctional(int x, int y) {
		// Check upper-left finder pattern
		if(x <= (int) Math.round(imgFinderLoc.get(0).x + 5 * modWidth) && y <= (int) Math.round(imgFinderLoc.get(0).y + 5 * modWidth)) {
			return true;
		}
		
		// Check lower-left finder pattern
		if(x <= (int) Math.round(imgFinderLoc.get(1).x + 5 * modWidth) && y >= (int) Math.round(imgFinderLoc.get(1).y - 4 * modWidth)) {
			return true;
		}
		
		// Check upper-right finder pattern
		if(x >= (int) Math.round(imgFinderLoc.get(2).x - 4 * modWidth) && y <= (int) Math.round(imgFinderLoc.get(2).y + 5 * modWidth)) {
			return true;
		}
		
		// Check alignment pattern
		if (versionInfo > 1) {
			int centerX = (int) Math.round(imgFinderLoc.get(0).x - 3 * modWidth + (18 + 4 * (versionInfo - 2)) * modWidth);
			int centerY = (int) Math.round(imgFinderLoc.get(0).y - 3 * modWidth + (18 + 4 * (versionInfo - 2)) * modWidth);
			
			if(Math.abs(centerX - x) <= (2 * modWidth) && Math.abs(centerY - y) <= (2 * modWidth)) {
				return true;
			}
		}
		
		return false;
	}
	
	private static boolean isQuietZone(int x, int y) {
		if(x < (int) Math.round(imgFinderLoc.get(0).x - 3 * modWidth) || x > (int) Math.round(imgFinderLoc.get(2).x + 3 * modWidth)) {
			return true;
		}

		if(y < (int) Math.round(imgFinderLoc.get(0).y - 3 * modWidth) || y > (int) Math.round(imgFinderLoc.get(1).y + 3 * modWidth)) {
			return true;
		}
		
		return false;
	}
	
	private static boolean checkFinderRatio(int[] accPixels) {
		int totalPixSize;
		int modSize;
		int modRatio;
		int maxVar;
		
		totalPixSize = 0;
		
		for(int i = 0; i < 5; i++) {
			if(accPixels[i] < 1) {
				return false;
			}
			
			totalPixSize += accPixels[i];
		}
		
		if(totalPixSize < 7) {
			return false;
		}
		
		modSize = (int) (totalPixSize / 7.0);
		maxVar = modSize / 2;
		
		for(int i = 0; i < 5; i++) {
			if(i == 2) {
				modRatio = 3 * modSize;
			}
			else {
				modRatio = modSize;
			}
			
			if(Math.abs(modRatio - accPixels[i]) > maxVar) {
				return false;
			}
		}
		
		return true;
	}
	
	private static Vector <Integer[]> tryFinderCol(int col) {
		int[] pixelState = {0, 0, 0, 0, 0};
		int nowState;
		int pixVal;
		int totalPixels;
		Vector <Integer[]> rowCoords;
		
		nowState = 0;
		rowCoords = new Vector <Integer[]> ();
		
		for(int i = 0; i < imgMatBW.rows(); i++) {
			pixVal = (int) imgMatBW.get(i, col)[0];
			
			if(nowState == 4 && pixVal == BYTE_WHITE) {
				totalPixels = 0;
				
				for(int r : pixelState) {
					totalPixels += r;
				}
				
				if(checkFinderRatio(pixelState)) {
					Integer[] rowSpan = {i - totalPixels + 1, i - 1};
					rowCoords.add(rowSpan);
					
					for(int r = 0; r < 5; r++) {
						pixelState[r] = 0;
					}
				}
				else {
					nowState = 3;
					
					pixelState[0] = pixelState[2];
					pixelState[1] = pixelState[3];
					pixelState[2] = pixelState[4];
					pixelState[3] = 1;
					pixelState[4] = 0;
				}
			}
			else if((nowState == 0 || nowState == 2) && pixVal == BYTE_WHITE) {
				nowState++;
			}
			else if((nowState == 1 || nowState == 3) && pixVal == BYTE_BLACK) {
				nowState++;
			}
			
			pixelState[nowState]++;
		}
		
		return rowCoords;
	}
	
	private static boolean checkLeftDiagRatio(int rowCenter, int colCenter) {
		int rowStart;
		int colStart;
		int pixVal;
		int totalPixels;
		int nowState;
		int j;
		int[] pixelState = {0, 0, 0, 0, 0};
		
		if(colCenter < rowCenter) {
			rowStart = (int) (rowCenter - colCenter);
			colStart = 0;
		}
		else if(colCenter > rowCenter) {
			rowStart = 0;
			colStart = (int) (colCenter - rowCenter);
		}
		else {
			rowStart = 0;
			colStart = 0;
		}
		
		nowState = 0;
		j = colStart;
		
		for(int i = rowStart; i < imgMatBW.rows() && j < imgMatBW.cols(); i++) {
			pixVal = (int) imgMatBW.get(i, j)[0];
			
			if(nowState == 4 && pixVal == BYTE_WHITE) {
				totalPixels = 0;
				
				for(int r : pixelState) {
					totalPixels += r;
				}
				
				if(checkFinderRatio(pixelState)) {
					if(Math.abs((int) rowCenter - (i - totalPixels + 1 + totalPixels / 2)) < 2 && Math.abs((int) colCenter - (j - totalPixels + 1 + totalPixels / 2)) < 2) {
						return true;
					}
				}
				else {
					nowState = 3;
					
					pixelState[0] = pixelState[2];
					pixelState[1] = pixelState[3];
					pixelState[2] = pixelState[4];
					pixelState[3] = 1;
					pixelState[4] = 0;
				}
			}
			else if((nowState == 0 || nowState == 2) && pixVal == BYTE_WHITE) {
				nowState++;
			}
			else if((nowState == 1 || nowState == 3) && pixVal == BYTE_BLACK) {
				nowState++;
			}
			
			pixelState[nowState]++;
			j++;
		}
		
		return false;
	}
	
	private static double getModuleWidth() {
		int nowState;
		int pixVal;
		int totalPixels;
		double modSize;
		int[] pixCount = {0, 0, 0};
		
		if(isOriented == false || imgFinderLoc.size() != 3) {
			Log.d("QRCode", "Set orientation first before finding module width.");
			return -1;
		}
		
		modSize = 0;
		
		for(int i = 0; i < 3; i++) {
			nowState = 0;
			
			for(int j = (int) imgFinderLoc.get(i).x; j < imgMatBW.cols(); j++) {
				Log.d("QRCode", "(" + imgFinderLoc.get(i).y + ", " +  j + ") in size " + imgMatBW.size().toString());
				Log.d("QRCode", (imgMatBW.get((int) imgFinderLoc.get(i).y, j) == null) + "");
				pixVal = (int) imgMatBW.get((int) imgFinderLoc.get(i).y, j)[0];
				
				if(nowState == 2 && pixVal > 128) {
					totalPixels = 0;
					
					for(int r : pixCount) {
						totalPixels += r;
					}
					
					// We are sure that these are finder patterns
					modSize += totalPixels;
					break;
				}
				else if(nowState == 0 && pixVal > 128) {
					nowState++;
				}
				else if(nowState == 1 && pixVal < 128) {
					nowState++;
				}
				
				pixCount[nowState]++;
			}
		}
		
		return modSize / 21;
	}
	
	private static boolean isUniqueFinderLoc(int row, int col) {
		for(Iterator <Point> it = imgFinderLoc.iterator(); it.hasNext(); ) {
			Point iElm = it.next();
			
			if(Math.abs(iElm.x - col) < 5 && Math.abs(iElm.y - row) < 5) {
				return false;
			}
		}
		
		return true;
	}
	
	public void drawFinderLoc() {
		for(int i = 0; i < imgFinderLoc.size(); i++) {
			Core.circle(imgMatBW, imgFinderLoc.get(i), 5, new Scalar(0x45), -1);
		}
	}
	
	public void connectTheDots() {
		if(imgFinderLoc.size() != 3) {
			Log.d("QRCode", "OMG! " + imgFinderLoc.size() + " position detection patterns were found. Cannot connect dots. :((");
			return;
		}
		
		for(int i = 0; i < 3; i++) {
			Core.line(imgMatBW, imgFinderLoc.get(i), imgFinderLoc.get((i + 1) % 3), new Scalar(0x88), 3);
		}
	}
}
